[4/n] add stricter typing in JsonPathStack#15
Conversation
Created using spr 1.3.6-beta.1
Created using spr 1.3.6-beta.1 [skip ci]
Created using spr 1.3.6-beta.1 [skip ci]
Created using spr 1.3.6-beta.1
ahl
left a comment
There was a problem hiding this comment.
Didn't look very carefully; please let me know if something needs particular scrutiny
Created using spr 1.3.6-beta.1
Detect cycles by checking for path segments (with a trailing `/`) rather than just using a raw string prefix. We hit this in Omicron where `AuditLogEntry` was incorrectly detected as a prefix of `AuditLogEntryActor`, hiding incompatible changes. This is a minimal fix that (I believe) is correct given the current structure. I'm reworking this in #15 to have stronger types so we can think about this in a more principled manner.
Created using spr 1.3.6-beta.1
Created using spr 1.3.6-beta.1
| /// Lifecycle of a schema comparison, keyed by `VisitedKey` in | ||
| /// [`Compare::visit_state`]. | ||
| #[derive(Clone, Debug)] | ||
| pub(crate) enum VisitState { | ||
| /// Comparison is in progress somewhere up the call stack. Re-entering a | ||
| /// key in this state means the schema graph has a cycle. | ||
| Visiting, | ||
| /// Comparison has completed. | ||
| Completed { | ||
| /// Whether the two schemas were determined to be equal. | ||
| equal: bool, | ||
| }, | ||
| } |
There was a problem hiding this comment.
In this PR I replaced the existing cycle detection check, which relied on looking at the stack of schemas, with a more explicit DFS postorder check.
This is equivalent to the old code:
if old_schema.context().stack().contains_cycle()
&& new_schema.context().stack().contains_cycle()
{
return Ok(true);
}Why? Basically because having a prefix in the stack is equivalent to being in the visiting state while performing the DFS. The tests added in #23 don't have any output differences, which is a reasonable sign that there aren't any behavior changes here.
I like how explicit the postorder check is, and it also fits quite nicely into visit_state.
| // Returning `true` on `Visiting` is sound only because the compare | ||
| // methods call `schema_push_change` eagerly on detecting a change | ||
| // rather than making decisions based on the value returned from this | ||
| // function. If that weren't the case -- for example, if there were | ||
| // code which did something like: | ||
| // | ||
| // let inner_eq = self.compare_schema(...)?; | ||
| // if !inner_eq { | ||
| // self.schema_push_change(...); | ||
| // } | ||
| // | ||
| // Then, relying on `true` here would result in a false negative. | ||
| // This invariant must be upheld by all compare methods. |
There was a problem hiding this comment.
Note this block -- the old code also returned true, which is correct, but I wanted to reason about this more clearly.
A
JsonPathStackalways has an endpoint at the bottom and a series of schemas above that. We're going to rely on this in upcoming work -- express this constraint in the type system.